include(`blogspot.m4')dnl
Недавно участвовал в конкурсе LINK(http://igdc.ru/viewpage.php?page_id=88, IGDC), писал игру на DEmbro. Это был достаточно интересный опыт использования на практике своего языка.

Сразу оговорюсь, что в DEmbro полно недоделок, он глючный, и я плохо на нём программирую :)

CUT
BOLD(Средства разработки)

DEmbro и текстовый редактор Vim. Выглядит это как-то так:
IMG(http://i057.radikal.ru/1107/a1/cd479c1c4f6e.png)

Из средств отладки — печать значений на консоль, комментирование подозриетльных фрагментов кода, и возможность без запуска всей игры загрузить какой-то отдельный модуль в REPL режиме и быстро посмотреть как себя ведут команды.

BOLD(Коротко о языке)

Я использовал (за исключением нескольких незначительных мест) только три типа: целое число, указатель и строка. Хотя типов как таковых в DEmbro нет. Все операции производятся на стеке, для чисел и указателей один стек, для строк sTIRET отдельный. Круглые скобки используются для многострочных комментариев, а для однострочных sTIRET общепринятое QUOTE(//).

Несколько примеров:
CODE
1 // положить на стек число 1
2 // положить на стек число 2
3 4 5 // положить на стек числа 3, 4 и 5. Теперь стек выглядит так: 1 2 3 4 5
+ // сложить два верхних числа. Теперь стек выглядит так: 1 2 3 9
* // умножить два верхних числа. Теперь стек выглядит так: 1 2 27
swap // поменять два верхних числа местами, 1 27 2
div // нацело поделить два верхних числа, 1 13
max // из двух верхних элементов оставить максимальный, 13
1+ // увеличить верхнее число на 1, получится 14
drop // скинуть верхний элемент со стека, теперь стек вернулся в начальное состояние
END

Можно писать свои команды при помощи конструкции
CODE
: name ..... ;
END
где name sTIRET имя команды.

(Именем может быть любая последовательность символов без пробелов. Никаких ограничений на имя нет (можно переопределять стандартные слова).)

Например, в программе всё измеряется в миллесекундах. Но иногда приходится задавать время в минутах. Чтобы задать пять минут, нужно 5 умножить на 60*1000, и получится соответствующее число миллисекунд. Чтобы делать это вычисление автоматически, можно написать команду
CODE
: mins 60000 * ;
END

Теперь можно просто записать
CODE
5 mins
END
и получить время в миллесекундах для пяти минут.

Команды можно выполнять косвенно. При помощи символа апостроф, за которым идёт название команды, можно положить указатель на эту команду. А при помощи команды execute выполнить указатель с вершины стека. Т.е.
CODE
5 ' mins execute
END
эквивалентно коду QUOTE(5 mins).

Можно создавать анонимные команды при помощи команды QUOTE(:noname):
:noname ." Hello world" cr ;
После выполнения этого кода на стеке будет лежать указатель на команду, который можно скормить команде execute на выполнение. Так, например:
:noname ." Hello world" cr ; execute

Пара примеров на работу с указателями и переменными:
CODE
variable x // создаёт область памяти под целое число или указатель
// теперь вызов команды x кладёт на стек указатель на эту область памяти
5 x ! // записать число 5 в переменную x
x @ // положить в стек значение переменной x
. // распечатать на консоль полученное число
x off // записать в переменную x ноль
END

Ещё есть условный оператор и циклы.
CODE
: printbool if ." TRUE" else ." FALSE" then cr ; // создаём команду printbool
// она снимает со стека верхний элемент, и если он ноль, то печатает FALSE
// иначе TRUE
false printbool // напечатает FALSE
true printbool // напечатает TRUE
100 10 > printbool // напечатает TRUE
100 10 < printbool // напечатает FALSE
100 0<> printbool // напечатает TRUE

// печатает числа от 0 до 10
:noname 0 begin dup 10 <= while dup . 1+ repeat drop ; execute
END

Условные операторы и циклы являются компилирующими командами, и потому их можно использовать только внутри описания команд.

Чтобы не затягивать вступление до бесконечности, дам напоследок несколько ссылок
LIST
  ITEM(LINK(http://code.google.com/p/dforth/wiki/DEmbro, Пишущееся вики))
  ITEM(LINK(http://code.google.com/p/dforth/source/browse/trunk/release/core/default/op.de, Файл с реализацией условного оператора и простых циклов))
  ITEM(LINK(http://code.google.com/p/dforth/source/browse/trunk/release/units/statements/switch.de, Файл с реализацией оператора switch))
  ITEM(LINK(http://code.google.com/p/dforth/source/browse/trunk/release/core/default/voc.de, Файл с реализацией работы с пространствами имён))
END dnl

Практически никакого контроля нет ни на стадии компиляции, ни на стадии интерпретации. Что лежит на стеке или в памяти узнать нельзя. Можно написать на свой страх и риск почти любую фигню с непредсказуемым исходом.

BOLD(Кратко об игре)

Лучше один раз увидеть, чем сто раз услышать, так что отсылаю читателя самому сыграть в LINK(http://igdc.ru/viewpage.php?page_id=88, игру) (запускать надо Doj\mazejourney\release\run.bat).

Я написал на DEmbro создание окна и инициализацию OpenGL достаточно давно. Т.к. числа с плавающей точкой в DEmbro немного сыроваты (в частности, нет нормального способа передать double в виде параметра в dll функцию), я решил для упрощения жизни их не использовать вообще. Кроме того, я не писал ни поддержки текстур, ни каких-то хитрых примитивов. Единственное, что я использовал при рендере sTIRET цветные квадраты.

Игру я писал с девизом QUOTE(каждый уровень имеет свой уникальный геймплей). Поэтому нужно было иметь удобные конструкции для описания уровней.

BOLD(Как описывается уровень)

Чтобы не томить любителей ковыряться самостоятельно, рекомендую посмотреть файл QUOTE(game\levels\big.de) sTIRET он является очень простым примером того, как описывается уровень. Кстати, по-поводу этого уровня, в нём можно усмотреть букву O sTIRET это чит, позволяющий пройти в определённом месте сквозь стену и оказаться у выхода.

Итак, первым делом я написал команду lab, которая позволяет в наглядном текстовом виде описывать уровни. Например, так:
CODE
lab 
##########
#S       #
#        #
#        #
#        #
#        #
#       F#
##########
\lab
END

(Из-за бага в ядре дембро, после слова QUOTE(lab) нужно ставить пробел.)

Команда lab использует функцию ядра QUOTE(source-next-line), которая читает следующую строку исходника. По первой строке определяется ширина уровня, а дальше создаёт двумерный массив, содержащий ascii коды символов. После выполнения этой конструкции можно при помощи команды last-lab-x получить ширину лабиринта, при помощи last-lab-y высоту лабиринта, а при помощи last-lab-maze указатель на двумерный массив.

Далее я написал команду QUOTE(level), которая создаёт уровень. Напрямую она практически не используется, но в файле QUOTE(game\levels\list.de) определяется надстройка над ней sTIRET newlevel, которая внутри себя вызывает level, выполняет файл, имя которого следует после QUOTE(newlevel), и связывает уровни в последовательность. В том же файле list.de можно обнаружить сам список уровней.

Как только мы создали уровень (командой level) можно приступать к его наполнению. Команда QUOTE(maze!) установит на текущем уровне последний лабиринт, созданный командой QUOTE(lab). При помощи команд QUOTE(cell-x!) и QUOTE(cell-y!) можно установить размер в пикселях одной ячейки лабирнта текущего уровня, например так:
16 cell-x! 16 cell-y!
При помощи команд QUOTE(offset-x!) и QUOTE(offset-y!) задаётся положение лабиринта на экране. Т.к. всегда мне нужно было центрировать лабирит, я написал команду QUOTE(centrize), которая автоматически вызывает QUOTE(offset-x!) и QUOTE(offset-y!), вычисляя смещение при помощи размера лабиринта и размера ячейки. 
При помощи команд QUOTE(player-x!) и QUOTE(player-y!) можно установить координаты ячейки, в которой будет появляться игрок в начале уровня.

Перейдём к более нетривиальным элементам уровня sTIRET событиям. Можно описать команды, которые будут выполняться при старте уровня sTIRET при помощи этого механизма запускается звук гонга перед началом уровня:
CODE
:noname pchar" data\sounds\newlevel.wav" sound ;init
END

Тут просто вызывается команда sound, которая принимает один параметр sTIRET указатель на pchar-строку, содержащую имя звукового файла.

Менее тривиальный примеры sTIRET события, когда игрок входит на какую-то ячейку. Можно установить на каждый тип (т.е. ascii-символ) ячейки свой обработчик. Обработчику на стеке передаются x,y координаты ячейки, на которую пытается войти игрок. Если их скинуть со стека, то игрок не сможет зайти в ячейку, т.е. можно описать обработчик для стены:
CODE
:noname drop drop ;enter_ #
END

Есть команда passed, которая, наоборот, считывает x,y и смещает в них игрока. Можно написать
CODE
:noname passed ;enter_ O
END

Тогда игрок сможет заходить в букву O. Чтобы буква O выглядела как обычная стена, для неё нужно определить такой же обработчик рисования (который тоже принимает координаты x,y), как и для стены:
CODE
: draw_# COLOR 2dup PURPLES GEN[]2 ^ set-color draw-cell-rect ;
:noname  draw_# ;draw_ #
:noname  draw_# ;draw_ O
END

Есть ещё один тип обработчиков для каждой ячейки sTIRET обработчик инициализации. Обработчики инициализации нужно установить перед вызовом команды QUOTE(lab). Тогда при считывании каждой ячейки внутри конструкции QUOTE(lab .... \lab) будет вызван соответствующий заданному ascii-символу обработчик инициализации.

Пример использования: всегда неудобно задавать положение игрока координатами при помощи QUOTE(player-x!) и QUOTE(player-y!). Гораздо удобнее пометить нужную ячейку буквой S. Для этого перед выполнением QUOTE(lab) достаточно добавить обработчик инициализации ячейки S:
CODE
:noname  player-y! player-x! ;oninit_ S
END

При входе на букву F по умолчанию задан переход к следующему уровню командой QUOTE(next). При желании аналогичной функциональностью можно наделить другие буквы, и сделать переходы на другие уровни.

Таковы общие принципы создания уровней. Теперь я разберу особенности разработки некоторых уровней. Часто, из-за копипасты в описнии уровней можно увидеть много ненужного кода, sTIRET я не успел его удалить до дедлайна.

BOLD(Уровень в темноте (dark.de))

На этом уровне игрок видит только соседние с собой клетки. Нужно просто при рисовании стены посчитать расстояние игрока до стены, и если оно больше одного, то не рисовать стену:
CODE
:
  2dup player-y @ - abs swap player-x @ - abs max 1 > if drop drop exit then
  COLOR 2dup PURPLES GEN[]2 ^ set-color draw-cell-rect 
;draw_ #
END

BOLD(Уровень-цикл (loop.de))

Тоже уровень в темноте, в котором если ходить по часовой стрелке, то будешь бегать по кругу, а если пойти против часовой стрелки, то прийдёшь к выходу. Реализация простая: описываются при помощи команды QUOTE(lab) три лабиринта sTIRET тот, который изначально, тот, который с циклом, и тот, который с выходом. Изначальный нужен, чтобы закрутить игрока идти по часовой стрелки, его мы устанавливаем в уровне командой QUOTE(maze!). Остальные два сохраняем в переменные. Вот, например, цикличный лабиринт:
CODE
lab 
     #######
     #1    #
###### ### #
#      # # #
# ###### # #
# #      # #
# #      # #
# ######## #
#     2    #
############
\lab
  last-lab-x value 1maze-x
  last-lab-y value 1maze-y
  last-lab-maze value 1maze-ptr
END

На карте есть ячейки 1 и 2. При наступании на 1 включается лабиринт с циклом, при наступании на 2 sTIRET лабиринт с выходом. Вот их обработчики:
CODE
:noname passed 1maze-x maze-x ! 1maze-y maze-y ! 1maze-ptr maze-ptr ! ;enter_ 1
:noname passed 2maze-x maze-x ! 2maze-y maze-y ! 2maze-ptr maze-ptr ! ;enter_ 2
END

Проверка видимости сделана так же, как и в dark.de, только расстояние берётся не 1, а 3 ячейки.

BOLD(Уровень с убегающим выходом (runningexit.de))

Алгоритм убегания выхода простой. Если игрок приблизился на близкое расстояние по обеим координатам, то выход смещается. Выбирается координата, по которой расстояние до игрока больше, после чего выход смещается от игрока по этому направлению. Если это не удаётся (стена), пытаемся сместить выход в перпендикулярном направлении.

Поэтому, например, если игрок идёт на выход по узкому коридору, то выход будет убегать по этому коридору от игрока, и свернёт в бок, как только упрётся в стену.

Там используется событие after-update для всех этих расчётов и рендера выхода (рендер сделан отдельный, потому что выход при смещении с ячейки на ячейку должен смещаться плавно). Когда выход смещается, в лабиринте перезаписывается его расположение, поэтому событие попадания в выход обрабатывается корректно обычным образом. 

BOLD(Уровень с двигающимися стенами (movingwalls.de))

Идея аналогична уровню-циклу. Я создал два лабиринта: гланый прямоугольник с выходом, и лабиринт со штуковиной. И сохранил его в переменные (moving-x, moving-y, moving-ptr).

Далее, я переопределил события рендера и входа для побела:
CODE
: draw#brown COLOR 2dup BROWNS GEN[]2 ^ set-color draw-cell-rect ;
:noname  2dup moving-x * + player-y @ + cells moving-ptr + @ 
         35 = if draw#brown else 2drop then ;draw_  
:noname  2dup moving-x * + player-y @ + cells moving-ptr + @ 
         35 = if 2drop else passed then ;enter_  
END

(Там в конце, каждого определения должно быть по два пробела.)

Число 35 sTIRET это ascii код для символа решётки (который я получил при помощи команды QUOTE(ga) в виме). Тут всё просто: чтобы определить нужно ли рисовать на пустом месте что-то, берётся значение из вспомогательного лабиринта со штуковиной, с прибавлением y-координаты игрока.

BOLD(Зкалючение)

В целом, разрабатывать игру мне очень понравилось.

Разработка игры длилась суммарно около 10ти дней, большинство из которых я ещё и ходил на работу. При этом первая половина этого времени была потрачена на написание основного функционала, а вторая на придумывание и реализацию уровней.
END
